﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Windows.Forms;
using ESRI.ArcGIS.DataSourcesRaster;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geometry;

namespace BesAsm.Emgaats.Maintenance
{
    public class UICommitLinksButton : ESRI.ArcGIS.Desktop.AddIns.Button
    {
        private ApplicationWindow _appWindow;
        private HashSet<int> _nodesAtDeletedLinkEnds;

        public UICommitLinksButton()
        {
            _appWindow = new ApplicationWindow(ArcMap.Application.hWnd);
        }

        protected override void OnClick()
        {
            System.Windows.Forms.Cursor.Current = Cursors.WaitCursor;
            try
            {
                string message = string.Empty;
                string nl = Environment.NewLine;

                DialogResult result = MessageBox.Show(_appWindow, "Are you sure you want to commit changes to the Master Links data?", "Commit Changes", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
                if (result == DialogResult.Yes)
                {
                    int totalErrors, totalWarnings;
                    EmgaatsMaintenanceExtension.Extension.CheckEditLayers(out totalErrors, out totalWarnings);

                    if (totalErrors > 0)
                    {
                        if (totalErrors == 1)
                            message = "Could not commit changes, because there is still 1 error. Check the attribute tables for more details.";
                        else
                            message = string.Format("Could not commit changes, because there are still {0} errors. Check the attribute tables for more details.", totalErrors);
                        MessageBox.Show(_appWindow, message, "Commit Aborted", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        return;
                    }

                    if (totalWarnings > 0)
                    {
                        if (totalWarnings == 1)
                            message = "There is 1 warning associated with the pending changes. Are you sure you want to continue and commit anyway?";
                        else
                            message = string.Format("There are {0} warnings associated with the pending changes. Are you sure you want to continue and commit anyway?", totalWarnings);
                        DialogResult userResponse = MessageBox.Show(_appWindow, message, "Warnings Present", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
                        if(userResponse != DialogResult.Yes)
                            return;
                    }

                    int linksInserted, linksRemoved, nodesInserted, nodesRemoved;
                    this._CommitChanges(out linksInserted, out linksRemoved, out nodesInserted, out nodesRemoved);

                    ArcMap.Document.ActiveView.Refresh();

                    message = "Changes to the Master Links have been committed." + nl;
                    message += nl + string.Format(linksRemoved == 1 ? "{0} link was removed." : "{0} links were removed.", linksRemoved);
                    message += nl + string.Format(linksInserted == 1 ? "{0} link was inserted." : "{0} links were inserted.", linksInserted);
                    message += nl + string.Format(nodesRemoved == 1 ? "{0} node was removed." : "{0} nodes were removed.", nodesRemoved);
                    message += nl + string.Format(nodesInserted == 1 ? "{0} node was inserted." : "{0} nodes were inserted.", nodesInserted);
                    MessageBox.Show(_appWindow, message, "Changed Committed", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
            }
            catch (Exception ex)
            {
                string msg = string.Format("An error occured: {0}", ex.Message);
                MessageBox.Show(_appWindow, msg, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                System.Windows.Forms.Cursor.Current = Cursors.Default;
            }
        }

        private void _CommitChanges(out int linksInserted, out int linksRemoved, out int nodesInserted, out int nodesRemoved)
        {
            linksInserted = 0;
            linksRemoved = 0;
            nodesInserted = 0;
            nodesRemoved = 0;

            IFeatureClass masterLinkClass = EmgaatsMaintenanceExtension.Extension.Configuration.MasterLinksEditClass;

            IWorkspaceEdit2 editWorkspace = ((IDataset)masterLinkClass).Workspace as IWorkspaceEdit2;
            IMultiuserWorkspaceEdit multiUserEdit = editWorkspace as IMultiuserWorkspaceEdit;

            if (multiUserEdit == null)
                throw new InvalidOperationException("The underlying database for the master links does not support multi-user editing.");

            bool sessionStarted = false;
            if (!editWorkspace.IsBeingEdited())
            {
                multiUserEdit.StartMultiuserEditing(esriMultiuserEditSessionMode.esriMESMNonVersioned);
                sessionStarted = true;
            }
            try
            {
                if (editWorkspace.IsInEditOperation)
                    throw new InvalidOperationException("Cannot edit master links database because it is currently being edited (already in edit operation).");

                _nodesAtDeletedLinkEnds = new HashSet<int>();

                _ProcessPendingArchives(out linksRemoved);
                _ProcessPendingAppends(out linksInserted, out nodesInserted);
                _CleanupNodes(out nodesRemoved);

                if (sessionStarted && editWorkspace.IsBeingEdited())
                    editWorkspace.StopEditing(true);
            }
            catch
            {
                if (sessionStarted && editWorkspace.IsBeingEdited())
                    editWorkspace.StopEditing(false);
                throw;
            }
        }

        private void _ProcessPendingArchives(out int linksRemoved)
        {
            linksRemoved = 0;

            IFeatureClass archiveClass = EmgaatsMaintenanceExtension.Extension.Settings.ArchiveClass;

            int idField = archiveClass.FindField("link_id");
            int userField = archiveClass.FindField("edited_by");
            int reasonField = archiveClass.FindField("edit_reason");
            int editDateField = archiveClass.FindField("edit_date");

            Dictionary<int, LinkInfo> archiveLinks = new Dictionary<int, LinkInfo>(archiveClass.FeatureCount(null));
            IFeatureCursor cursor = archiveClass.Search(null, false);
            try
            {
                IFeature archiveFeature = cursor.NextFeature();
                while (archiveFeature != null)
                {
                    try
                    {
                        int id = (int)archiveFeature.get_Value(idField);
                        string user = archiveFeature.get_Value(userField) as string;
                        string reason = archiveFeature.get_Value(reasonField) as string;
                        DateTime date = (DateTime)archiveFeature.get_Value(editDateField);

                        archiveLinks.Add(id, new LinkInfo() { Id = id, User = user, Reason = reason, EditDate = date });
                    }
                    finally
                    {
                        EmgaatsMaintenanceExtension.ReleaseComObject(archiveFeature);
                    }

                    archiveFeature = cursor.NextFeature();
                }
            }
            finally
            {
                EmgaatsMaintenanceExtension.ReleaseComObject(cursor);
            }

            if (archiveLinks.Count > 0)
            {
                IFeatureClass masterLinkClass = EmgaatsMaintenanceExtension.Extension.Configuration.MasterLinksEditClass;
                idField = masterLinkClass.FindField("link_id");
                userField = masterLinkClass.FindField("edited_by");
                reasonField = masterLinkClass.FindField("edit_reason");
                editDateField = masterLinkClass.FindField("edit_date");
                int statusField = masterLinkClass.FindField("status");
                int upNodeIdField = masterLinkClass.FindField("us_node_id");
                int downNodeIdField = masterLinkClass.FindField("ds_node_id");

                IWorkspaceEdit2 editWorkspace = ((IDataset)masterLinkClass).Workspace as IWorkspaceEdit2;
                editWorkspace.StartEditOperation();
                try
                {
                    IQueryFilter filter = new QueryFilterClass();
                    filter.WhereClause = "status = 'ACT'";

                    cursor = masterLinkClass.Update(filter, false);
                    try
                    {
                        IFeature linkFeature = cursor.NextFeature();
                        while (linkFeature != null)
                        {
                            try
                            {
                                int id = (int)linkFeature.get_Value(idField);
                                if (archiveLinks.ContainsKey(id))
                                {
                                    _nodesAtDeletedLinkEnds.Add((int)linkFeature.get_Value(upNodeIdField));
                                    _nodesAtDeletedLinkEnds.Add((int)linkFeature.get_Value(downNodeIdField));

                                    linkFeature.set_Value(userField, archiveLinks[id].User);
                                    linkFeature.set_Value(reasonField, archiveLinks[id].Reason);
                                    linkFeature.set_Value(editDateField, archiveLinks[id].EditDate);
                                    linkFeature.set_Value(statusField, "RET");

                                    linkFeature.Store();
                                    linksRemoved++;
                                }
                            }
                            finally
                            {
                                EmgaatsMaintenanceExtension.ReleaseComObject(linkFeature);
                            }

                            linkFeature = cursor.NextFeature();
                        }
                    }
                    finally
                    {
                        EmgaatsMaintenanceExtension.ReleaseComObject(cursor);
                    }

                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.StopEditOperation();
                }
                catch
                {
                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.AbortEditOperation();
                    throw;
                }
            }
        }

        private void _ProcessPendingAppends(out int linksInserted, out int nodesInserted)
        {
            linksInserted = 0;
            nodesInserted = 0;

            IFeatureClass appendClass = EmgaatsMaintenanceExtension.Extension.Settings.AppendClass;
            if (appendClass.FeatureCount(null) > 0)
            {
                // Any new nodes must be inserted first, because of foreign key constraint
                HashSet<string> newNodes = _InsertNewNodes(appendClass, out nodesInserted);

                IFeatureClass linkClass = EmgaatsMaintenanceExtension.Extension.Configuration.MasterLinksEditClass;
                FieldMapping fieldMapping = EmgaatsMaintenanceExtension.Extension.Configuration.LinkFieldMapping;

                int idField = linkClass.FindField("link_id");
                int linkNameField = linkClass.FindField("link_name");
                int upNodeNameField = linkClass.FindField("us_node_name_deprecated");
                int downNodeNameField = linkClass.FindField("ds_node_name_deprecated");
                int upNodeIdField = linkClass.FindField("us_node_id");
                int downNodeIdField = linkClass.FindField("ds_node_id");

                int nodeId = -1;

                List<int> insertedIds = new List<int>(appendClass.FeatureCount(null));

                IWorkspaceEdit2 editWorkspace = ((IDataset)linkClass).Workspace as IWorkspaceEdit2;
                editWorkspace.StartEditOperation();
                try
                {
                    IFeatureCursor appendCursor = null;
                    try
                    {
                        appendCursor = appendClass.Search(null, false);

                        IFeature appendFeature = appendCursor.NextFeature();
                        while (appendFeature != null)
                        {
                            IFeature linkFeature = linkClass.CreateFeature();
                            try
                            {
                                string upNodeName = string.Empty;
                                string downNodeName = string.Empty;

                                // Copy shape
                                linkFeature.Shape = appendFeature.ShapeCopy as IPolyline;

                                // Copy all attributes
                                for (int f = 0; f < linkFeature.Fields.FieldCount; f++)
                                {
                                    IField toField = linkFeature.Fields.get_Field(f);
                                    if (toField.Type != esriFieldType.esriFieldTypeOID && toField.Type != esriFieldType.esriFieldTypeGeometry && toField.Editable)
                                    {
                                        if (fieldMapping.Contains(toField.Name) && !fieldMapping[toField.Name].Ignore)
                                        {
                                            int fromFieldIndex = appendClass.FindField(toField.Name);
                                            if (fromFieldIndex > -1)
                                            {
                                                object dbValue = appendFeature.get_Value(fromFieldIndex);
                                                if (dbValue != DBNull.Value || toField.IsNullable)
                                                {
                                                    linkFeature.set_Value(f, dbValue);
                                                }

                                                if (f == upNodeNameField)
                                                {
                                                    string nodeName = dbValue as string;
                                                    if (newNodes.Contains(nodeName))
                                                        upNodeName = nodeName;
                                                }

                                                if (f == downNodeNameField)
                                                {
                                                    string nodeName = dbValue as string;
                                                    if (newNodes.Contains(nodeName))
                                                        downNodeName = nodeName;
                                                }
                                            }
                                        }
                                    }
                                }

                                // Handle new node ids
                                // TODO: If node already exists, but is retired, we need to "reactivate" it by setting the status
                                if (!string.IsNullOrEmpty(upNodeName))
                                {
                                    nodeId = EmgaatsMaintenanceExtension.Extension.GetNodeId(upNodeName, false);
                                    if (nodeId > -1)
                                        linkFeature.set_Value(upNodeIdField, nodeId);
                                }

                                if (!string.IsNullOrEmpty(downNodeName))
                                {
                                    nodeId = EmgaatsMaintenanceExtension.Extension.GetNodeId(downNodeName, false);
                                    if (nodeId > -1)
                                        linkFeature.set_Value(downNodeIdField, nodeId);
                                }

                                // HACK: We use an encoded Guid to create a unique name so we don't violoate unique name constraint in database.
                                // This will be overwritten below with a string representation of the link_id after the edit operation is commited and
                                // the link_id is populated by the database.
                                string tempLinkName = CompactGuidEncoder.Encode(Guid.NewGuid());
                                linkFeature.set_Value(linkNameField, tempLinkName);

                                linkFeature.Store();
                                insertedIds.Add(linkFeature.OID);
                            }
                            finally
                            {
                                EmgaatsMaintenanceExtension.ReleaseComObject(linkFeature);
                                EmgaatsMaintenanceExtension.ReleaseComObject(appendFeature);
                            }
                            appendFeature = appendCursor.NextFeature();
                        }
                    }
                    finally
                    {
                        EmgaatsMaintenanceExtension.ReleaseComObject(appendCursor);
                    }
                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.StopEditOperation();
                }
                catch
                {
                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.AbortEditOperation();
                    throw;
                }

                // Update link_name now that the links have been inserted and link_id is populated
                if (insertedIds.Count > 0)
                {
                    editWorkspace.StartEditOperation();
                    try
                    {
                        IGeoDatabaseBridge databaseHelper = new GeoDatabaseHelperClass();
                        int[] ids = insertedIds.ToArray();
                        IFeatureCursor linkCursor = databaseHelper.GetFeatures(linkClass, ref ids, false);
                        try
                        {
                            IFeature linkFeature = linkCursor.NextFeature();
                            while (linkFeature != null)
                            {
                                try
                                {
                                    int linkId = (int)linkFeature.get_Value(idField);
                                    linkFeature.set_Value(linkNameField, linkId.ToString());

                                    linkFeature.Store();
                                }
                                finally
                                {
                                    EmgaatsMaintenanceExtension.ReleaseComObject(linkFeature);
                                }
                                linkFeature = linkCursor.NextFeature();
                            }
                        }
                        finally
                        {
                            EmgaatsMaintenanceExtension.ReleaseComObject(linkCursor);
                        }

                        if (editWorkspace.IsInEditOperation)
                            editWorkspace.StopEditOperation();
                    }
                    catch
                    {
                        if (editWorkspace.IsInEditOperation)
                            editWorkspace.AbortEditOperation();
                        throw;
                    }
                }

                linksInserted = insertedIds.Count;
            }
        }

        private HashSet<string> _InsertNewNodes(IFeatureClass appendClass, out int nodesInserted)
        {
            nodesInserted = 0;

            string nodeName;
            int nodeId;
            Dictionary<string, IPoint> newNodes = new Dictionary<string, IPoint>();

            int upNodeField = appendClass.FindField("us_node_name_deprecated");
            int downNodeField = appendClass.FindField("ds_node_name_deprecated");

            // Check for new nodes
            IFeatureCursor appendCursor = null;
            try
            {
                appendCursor = appendClass.Search(null, false);

                IFeature appendFeature = appendCursor.NextFeature();
                while (appendFeature != null)
                {
                    try
                    {
                        IPolyline line = appendFeature.Shape as IPolyline;

                        nodeName = appendFeature.get_Value(upNodeField) as string;
                        if (nodeName != null && !newNodes.ContainsKey(nodeName))
                        {
                            nodeId = EmgaatsMaintenanceExtension.Extension.GetNodeId(nodeName);
                            if (nodeId == -1)
                                newNodes.Add(nodeName, line.FromPoint);
                        }

                        nodeName = appendFeature.get_Value(downNodeField) as string;
                        if (nodeName != null && !newNodes.ContainsKey(nodeName))
                        {
                            nodeId = EmgaatsMaintenanceExtension.Extension.GetNodeId(nodeName);
                            if (nodeId == -1)
                                newNodes.Add(nodeName, line.ToPoint);
                        }
                    }
                    finally
                    {
                        EmgaatsMaintenanceExtension.ReleaseComObject(appendFeature);
                    }
                    appendFeature = appendCursor.NextFeature();
                }
            }
            finally
            {
                EmgaatsMaintenanceExtension.ReleaseComObject(appendCursor);
            }

            // If there are new nodes, insert them
            if (newNodes.Count > 0)
            {
                IFeatureClass nodeClass = EmgaatsMaintenanceExtension.Extension.Configuration.MasterNodesEditClass;
                int nameField = nodeClass.FindField("node_name");
                int xCoordField = nodeClass.FindField("x_coord");
                int yCoordField = nodeClass.FindField("y_coord");
                int typeField = nodeClass.FindField("node_type");
                int elevField = nodeClass.FindField("ground_elevation");
                int userField = nodeClass.FindField("edited_by");
                int reasonField = nodeClass.FindField("edit_reason");
                int statusField = nodeClass.FindField("status");
                int appendField = nodeClass.FindField("append_date");
                int editField = nodeClass.FindField("edit_date");
                int qualityField = nodeClass.FindField("data_quality");

                IRaster2 dem = EmgaatsMaintenanceExtension.Extension.Configuration.DEM as IRaster2;

                IWorkspaceEdit2 editWorkspace = ((IDataset)nodeClass).Workspace as IWorkspaceEdit2;
                editWorkspace.StartEditOperation();
                try
                {
                    foreach (KeyValuePair<string, IPoint> newNode in newNodes)
                    {
                        IPoint nodePoint = newNode.Value;

                        IFeature nodeFeature = nodeClass.CreateFeature();
                        try
                        {
                            // TODO: Use configuration information (add to configuration file)
                            // TODO: Check for equivalent DME node and use data (e.g. type) from there, if it exists
                            nodeFeature.Shape = nodePoint;
                            nodeFeature.set_Value(nameField, newNode.Key);
                            nodeFeature.set_Value(xCoordField, nodePoint.X);
                            nodeFeature.set_Value(yCoordField, nodePoint.Y);
                            nodeFeature.set_Value(typeField, "MH");
                            nodeFeature.set_Value(elevField, _GetElevation(nodePoint, dem));
                            nodeFeature.set_Value(userField, Environment.UserName);
                            nodeFeature.set_Value(reasonField, "DME");
                            nodeFeature.set_Value(statusField, "ACT");
                            nodeFeature.set_Value(appendField, DateTime.Now);
                            nodeFeature.set_Value(editField, DateTime.Now);
                            nodeFeature.set_Value(qualityField, "F");

                            nodeFeature.Store();
                            nodesInserted++;
                        }
                        finally
                        {
                            EmgaatsMaintenanceExtension.ReleaseComObject(nodeFeature);
                        }
                    }

                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.StopEditOperation();
                }
                catch
                {
                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.AbortEditOperation();
                    throw;
                }
            }

            return new HashSet<string>(newNodes.Keys);
        }

        private double _GetElevation(IPoint nodePoint, IRaster2 dem)
        {
            double elevation = 0.0;
            try
            {
                if (dem != null)
                {
                    int col = dem.ToPixelColumn(nodePoint.X);
                    int row = dem.ToPixelRow(nodePoint.Y);
                    elevation = Convert.ToDouble(dem.GetPixelValue(0, col, row));
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Error getting node elevation from DEM at ({0}, {1}).", nodePoint.X, nodePoint.Y));
                System.Diagnostics.Debug.WriteLine(ex.GetType().FullName + ": " + ex.Message);
            }
            return elevation;
        }

        private void _CleanupNodes(out int nodesRemoved)
        {
            nodesRemoved = 0;

            if (_nodesAtDeletedLinkEnds.Count > 0)
            {
                HashSet<int> referencedIds = new HashSet<int>();

                int nodeId = -1;

                IFeatureClass linkClass = EmgaatsMaintenanceExtension.Extension.Configuration.MasterLinksEditClass;
                int upNodeIdField = linkClass.FindField("us_node_id");
                int downNodeIdField = linkClass.FindField("ds_node_id");

                IQueryFilter filter = new QueryFilterClass();
                filter.WhereClause = "status = 'ACT'";

                IFeatureCursor linkCursor = linkClass.Search(filter, true);
                try
                {
                    IFeature linkFeature = linkCursor.NextFeature();
                    while (linkFeature != null)
                    {
                        try
                        {
                            nodeId = (int)linkFeature.get_Value(upNodeIdField);
                            referencedIds.Add(nodeId);

                            nodeId = (int)linkFeature.get_Value(downNodeIdField);
                            referencedIds.Add(nodeId);
                        }
                        finally
                        {
                            EmgaatsMaintenanceExtension.ReleaseComObject(linkFeature);
                        }

                        linkFeature = linkCursor.NextFeature();
                    }
                }
                finally
                {
                    EmgaatsMaintenanceExtension.ReleaseComObject(linkCursor);
                }

                IFeatureClass nodeClass = EmgaatsMaintenanceExtension.Extension.Configuration.MasterNodesEditClass;
                int nodeIdField = nodeClass.FindField("node_id");
                int statusField = nodeClass.FindField("status");
                int userField = nodeClass.FindField("edited_by");
                int reasonField = nodeClass.FindField("edit_reason");
                int editField = nodeClass.FindField("edit_date");

                IWorkspaceEdit2 editWorkspace = ((IDataset)nodeClass).Workspace as IWorkspaceEdit2;
                editWorkspace.StartEditOperation();
                try
                {
                    IFeatureCursor nodeCursor = nodeClass.Update(null, false);
                    try
                    {
                        IFeature nodeFeature = nodeCursor.NextFeature();
                        while (nodeFeature != null)
                        {
                            try
                            {
                                nodeId = (int)nodeFeature.get_Value(nodeIdField);
                                if (!referencedIds.Contains(nodeId) && _nodesAtDeletedLinkEnds.Contains(nodeId))
                                {
                                    nodeFeature.set_Value(statusField, "RET");
                                    nodeFeature.set_Value(userField, Environment.UserName);
                                    nodeFeature.set_Value(reasonField, "orphaned");
                                    nodeFeature.set_Value(editField, DateTime.Now);

                                    nodeFeature.Store();
                                    nodesRemoved++;
                                }
                            }
                            finally
                            {
                                EmgaatsMaintenanceExtension.ReleaseComObject(nodeFeature);
                            }
                            nodeFeature = nodeCursor.NextFeature();
                        }
                    }
                    finally
                    {
                        EmgaatsMaintenanceExtension.ReleaseComObject(nodeCursor);
                    }

                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.StopEditOperation();
                }
                catch
                {
                    if (editWorkspace.IsInEditOperation)
                        editWorkspace.AbortEditOperation();
                    throw;
                }
            }
        }

        protected override void OnUpdate()
        {
            bool enabled = false;
            if (ArcMap.Application != null &&
                EmgaatsMaintenanceExtension.Extension != null &&
                EmgaatsMaintenanceExtension.Extension.Settings != null &&
                EmgaatsMaintenanceExtension.Extension.Settings.AppendClass != null &&
                EmgaatsMaintenanceExtension.Extension.Settings.ArchiveClass != null)
            {
                enabled = true;
            }

            this.Enabled = enabled;
        }

        private struct LinkInfo
        {
            public int Id;
            public string User;
            public string Reason;
            public DateTime EditDate;
        }
    }
}
